package io.cici.cc.mybatis.lite.reflection;

import io.cici.cc.mybatis.lite.annotations.ColumnMapping;
import io.cici.cc.mybatis.lite.reflection.invoker.GetFieldInvoker;
import io.cici.cc.mybatis.lite.reflection.invoker.Invoker;
import io.cici.cc.mybatis.lite.reflection.invoker.MethodInvoker;
import io.cici.cc.mybatis.lite.reflection.invoker.SetFieldInvoker;
import io.cici.cc.mybatis.lite.type.TypeHandler;
import io.cici.cc.mybatis.lite.type.TypeHandlerRegistry;

import java.lang.reflect.*;
import java.util.*;
import java.util.Map.Entry;


public class Reflector {
    private final Class<?> clazz;
    private final String[] readablePropertyNames;
    private final String[] writablePropertyNames;
    private final Map<String, Invoker> setMethods = new HashMap<>();
    private final Map<String, Invoker> getMethods = new HashMap<>();
    private final Map<String, Class<?>> setTypes = new HashMap<>();
    private final Map<String, Class<?>> getTypes = new HashMap<>();
    private final Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
    private final List<ColumnInfo> columnInfoList = new ArrayList<>();
    private final Map<String, ColumnInfo> fieldNameToColumnInfoMap = new HashMap<>();
    private Constructor<?> defaultConstructor;

    public Reflector(Class<?> clazz) {
        this.clazz = clazz;
        addDefaultConstructor(clazz);

        Method[] methods = getClassMethods(clazz);
        Map<String, Field> fieldNameToFieldMap = getClassFields(clazz);
        Set<Entry<String, Field>> set = fieldNameToFieldMap.entrySet();
        for (Entry<String, Field> entry : set) {
            String fieldName = entry.getKey();
            if (isInValidPropertyName(fieldName)) {
                continue;
            }
            Field field = entry.getValue();
            ColumnMapping columnMapping = field.getAnnotation(ColumnMapping.class);
            if (columnMapping != null) {
                String columnName = columnMapping.value();
                String type = columnMapping.type();
                TypeHandler typeHandler = TypeHandlerRegistry.getTypeHandler(type);
                ColumnInfo columnInfo = new ColumnInfo();
                columnInfo.setColumnName(columnName);
                columnInfo.setType(type);
                columnInfo.setTypeHandler(typeHandler);
                //set/get方法 没有is开头的方法
                Method setMethod = null;
                Method getMethod = null;
                for (Method method : methods) {
                    String name = method.getName();
                    String firstChar = String.valueOf(fieldName.charAt(0)).toUpperCase();
                    if (name.equals("set" + firstChar + fieldName.substring(1))) {
                        if (method.getParameterCount() == 1) {
                            if (setMethod != null) {
                                throw new IllegalArgumentException("已经存在set方法了");
                            }
                            setMethod = method;
                        } else {
                            throw new IllegalArgumentException("不允许存在同名的set单参数不同的方法");
                        }
                    }
                    if (name.equals("get" + firstChar + fieldName.substring(1))) {
                        if (method.getParameterCount() == 0) {
                            if (getMethod != null) {
                                throw new IllegalArgumentException("已经存在get方法了");
                            }
                            getMethod = method;
                        } else {
                            throw new IllegalArgumentException("不允许存在同名的get单参数不同的方法");
                        }
                    }
                }
                if (setMethod == null) {
                    throw new NullPointerException();
                }
                if (getMethod == null) {
                    throw new NullPointerException();
                }
                columnInfo.setSetMethod(setMethod);
                columnInfo.setGetMethod(getMethod);
                //
                {
                    MethodInvoker setMethodInvoker = new MethodInvoker(setMethod);
                    setMethods.put(fieldName, setMethodInvoker);
                    Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setMethod, clazz);
                    setTypes.put(fieldName, typeToClass(paramTypes[0]));
                }

                {
                    setMethods.put(fieldName, new SetFieldInvoker(field));
                    Type fieldType = TypeParameterResolver.resolveFieldType(field, clazz);
                    setTypes.put(field.getName(), typeToClass(fieldType));
                }

                {
                    MethodInvoker getMethodInvoker = new MethodInvoker(getMethod);
                    getMethods.put(fieldName, getMethodInvoker);
                    Type returnType = TypeParameterResolver.resolveReturnType(clazz, getMethod);
                    getTypes.put(fieldName, typeToClass(returnType));
                }
                {
                    getMethods.put(field.getName(), new GetFieldInvoker(field));
                    Type fieldType = TypeParameterResolver.resolveFieldType(field, clazz);
                    getTypes.put(field.getName(), typeToClass(fieldType));
                }

                columnInfoList.add(columnInfo);
            }
        }
        if (columnInfoList.isEmpty()) {
            throw new IllegalArgumentException("不存在字段映射!");
        }
        for (ColumnInfo columnInfo : columnInfoList) {
            fieldNameToColumnInfoMap.put(columnInfo.getFieldName(), columnInfo);
        }


        readablePropertyNames = getMethods.keySet().toArray(new String[0]);
        writablePropertyNames = setMethods.keySet().toArray(new String[0]);
        for (String propName : readablePropertyNames) {
            caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
        }
        for (String propName : writablePropertyNames) {
            caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
        }
    }

    private void addDefaultConstructor(Class<?> clazz) {
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        Arrays.stream(constructors).filter(constructor -> constructor.getParameterTypes().length == 0).findAny().ifPresent(constructor -> this.defaultConstructor = constructor);
        if (this.defaultConstructor == null) {
            throw new RuntimeException("没有默认的构造方法");
        }
    }

    private boolean isInValidPropertyName(String name) {
        return (name.startsWith("$") || "serialVersionUID".equals(name) || "class".equals(name));
    }

    private Class<?> typeToClass(Type src) {
        Class<?> result = null;
        if (src instanceof Class) {
            result = (Class<?>) src;
            return result;
        } else if (src instanceof ParameterizedType) {
            throw new IllegalArgumentException();
        } else if (src instanceof GenericArrayType) {
            throw new IllegalArgumentException();
        } else if (src instanceof WildcardType) {
            throw new IllegalArgumentException();
        } else {
            throw new IllegalArgumentException();
        }

    }

    private Method[] getClassMethods(Class<?> clazz) {
        Map<String, Method> map = new HashMap<>();
        Class<?> currentClass = clazz;
        while (currentClass != null && currentClass != Object.class) {
            Method[] declaredMethods = currentClass.getDeclaredMethods();

            for (Method declaredMethod : declaredMethods) {
                if (map.containsKey(declaredMethod.getName())) {
                    throw new IllegalArgumentException();
                } else {
                    map.put(declaredMethod.getName(), declaredMethod);
                }
            }

            currentClass = currentClass.getSuperclass();
        }

        Collection<Method> methods = map.values();

        return methods.toArray(new Method[0]);
    }

    private Map<String, Field> getClassFields(Class<?> clazz) {
        Map<String, Field> map = new HashMap<>();
        Class<?> currentClass = clazz;
        while (currentClass != null && currentClass != Object.class) {
            Field[] fields = currentClass.getFields();
            for (Field field : fields) {
                if (map.containsKey(field.getName())) {
                    throw new IllegalArgumentException("");
                }
                map.put(field.getName(), field);
            }
            Class<?> superclass = currentClass.getSuperclass();
            currentClass = superclass;
        }
        return map;
    }


    public Class<?> getClazz() {
        return clazz;
    }

    public boolean hasDefaultConstructor() {
        return defaultConstructor != null;
    }

    public Invoker getSetInvoker(String propertyName) {
        Invoker method = setMethods.get(propertyName);
        if (method == null) {
            throw new ReflectionException(
                    "There is no setter for property named '" + propertyName + "' in '" + clazz + "'");
        }
        return method;
    }

    public Invoker getGetInvoker(String propertyName) {
        Invoker method = getMethods.get(propertyName);
        if (method == null) {
            throw new ReflectionException(
                    "There is no getter for property named '" + propertyName + "' in '" + clazz + "'");
        }
        return method;
    }

    public Class<?> getSetterType(String propertyName) {
        Class<?> clazz = setTypes.get(propertyName);
        if (clazz == null) {
            throw new ReflectionException(
                    "There is no setter for property named '" + propertyName + "' in '" + this.clazz + "'");
        }
        return clazz;
    }

    public Class<?> getGetterType(String propertyName) {
        Class<?> clazz = getTypes.get(propertyName);
        if (clazz == null) {
            throw new ReflectionException(
                    "There is no getter for property named '" + propertyName + "' in '" + this.clazz + "'");
        }
        return clazz;
    }

    public boolean hasSetter(String propertyName) {
        return fieldNameToColumnInfoMap.containsKey(propertyName);
    }

    public boolean hasGetter(String propertyName) {
        return fieldNameToColumnInfoMap.containsKey(propertyName);
    }

    public String findPropertyName(String name) {
        return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
    }

}
